/*
 * Copyright (c) 2012 Johan Fylling
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy 
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights 
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
 * copies of the Software, and to permit persons to whom the Software is 
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in 
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
 * THE SOFTWARE.
 */

part of engine;


typedef OnPauseListener (bool);


class GameEngineListeners {
  final List<OnPauseListener> pause = new List<OnPauseListener>();
}


class GameEngine {
  final GameEngineListeners _listeners               = new GameEngineListeners();
  bool                      _pause                   = false;
  final WorldState          worldState;
  Percept                   defaultPercept           = new Percept();
  final PhysicsEngine       _physicsEngine;
  final GraphicsEngine      _graphicsEngine;
  final CollisionManager    collisionManager;
  final Stopwatch           _stopwatch               = new Stopwatch();
  num                       _actorsUpdateDelta       = 0;
  num                       _effectsUpdateDelta      = 0;
  num                       _physicsUpdateDelta      = 0;
  num                       _graphicsUpdateDelta     = 0;
  num                       _collisionDetectionDelta = 0;
  
  GameEngine (this._physicsEngine, 
              this._graphicsEngine, 
              this.worldState,
              this.collisionManager) {
    StorageManager storageManager = (ServiceManager.getService("StorageManager") as StorageManager);
    if (storageManager == null) {
      storageManager = new EngineStorageManager(worldState, collisionManager);
      ServiceManager.setService("StorageManager", storageManager);
    }
  }
  
  GameEngineListeners get on => this._listeners;
  
  bool get pause => this._pause;
  
  set pause (bool pause) {
    this._pause = pause;
    for (OnPauseListener listener in this._listeners.pause) {
      listener(pause);
    }
  }
  
  num get actorsUpdateDelta => this._actorsUpdateDelta;
  
  num get effectsUpdateDelta => this._effectsUpdateDelta;
  
  num get physicsUpdateDelta => this._physicsUpdateDelta;
  
  num get graphicsUpdateDelta => this._graphicsUpdateDelta;
  
  num get collisionDetectionDelta => this._collisionDetectionDelta;
  
  void step (num deltaTime) {
    this._stopwatch.start();
    
    if (!this.pause) {
      this.worldState.updateClock(deltaTime);
      
      // Update Actor logic.
      this._stopwatch.reset();
      _updateActors(deltaTime);
      this._actorsUpdateDelta = this._stopwatch.elapsedMilliseconds;
      
      // Update Input Handler.
      InputManager inputManager = (ServiceManager.getService("InputManager") as InputManager);
      if (inputManager != null) {
        inputManager.update();
      }
      
      // Update effects.
      this._stopwatch.reset();
      _updateEffects(this.worldState);
      this._effectsUpdateDelta = this._stopwatch.elapsedMilliseconds;
      
      // Update physics.
      this._stopwatch.reset();
      this._physicsEngine.step(deltaTime, this.worldState);
      this._physicsUpdateDelta = this._stopwatch.elapsedMilliseconds;
      
      // Detect collisions.
      this._stopwatch.reset();
      this.collisionManager.detectCollisions(beforeContact: _beforeContact,
                                             handleCollision: _handleCollision);
      this._collisionDetectionDelta = this._stopwatch.elapsedMilliseconds;
    }
    
    // Update graphics.
    this._stopwatch.reset();
    this._graphicsEngine.step(this.worldState);
    this._stopwatch.stop();
    this._graphicsUpdateDelta = this._stopwatch.elapsedMilliseconds;
  }
  
  bool _beforeContact (DynamicBody body1,
                      DynamicBody body2) {
    return body1.active || body2.active;
  }
  
  void _handleCollision (DynamicBody body1,
                        DynamicBody body2,
                        ContactManifold manifold,
                        Vector2 penetration) {
    ContactPoint cp = manifold.deepest;
    if (cp == null) {
      return;
    }
    
    // TODO: clean this up.
    num mass1 = body1.mass;
    num mass2 = body2.mass;
    num massSum = mass1 + mass2;
    num weight1 = 0.0;
    num weight2 = 0.0;
    
    if (massSum == 0.0) {
      weight1 = 0.5;
      weight2 = 0.5;
    } else if (mass1 == 0.0) {
      weight2 = 1.0;
    } else if (mass2 == 0.0) {
      weight1 = 1.0;
    } else {
      weight1 = mass2 / massSum;
      weight2 = mass1 / massSum;
    }
    
    if (body1 is ParticleSystem) {
      this._physicsEngine.pushBody(this.worldState,
                                   body1, 
                                   -penetration.normalized()*cp.magnitude*weight1, 
                                   cp.position);
    }
    
    if (body2 is ParticleSystem) {
      this._physicsEngine.pushBody(this.worldState,
                                   body2, 
                                   penetration.normalized()*cp.magnitude*weight2, 
                                   cp.position);
    }
  }
  
  void _updateActors (num deltaTime) {
    num deltaTimeS = deltaTime / 1e3;
    
    for (Actor actor in this.worldState.actors) {
      Percept percept = (actor.percept != null ? actor.percept : defaultPercept);
      percept.init(actor, this.worldState, this.collisionManager);
      actor.controller.update(percept);
      
      for (Action action in actor.actions) {
        if (action is UseAction) {
          Actor tool = this.worldState.getActor(action.tool);
          if (tool != null) {
            Percept toolPercept = tool.percept;
            toolPercept.init(tool, this.worldState, this.collisionManager);
            tool.controller.use(toolPercept, percept);
          } else {
            print("Tool ${action.tool} not found");
          }
        }
      }
      actor.actions.clear();
      
      if (actor.body != null && deltaTime != 0)  {
        actor.body.angle = DynamicBody.baseOrientation.angle(actor.movementAction.orientation);
        num speed = (actor.movementAction.speed <= actor.maxSpeed ? actor.movementAction.speed : actor.maxSpeed);
        actor.body.velocity = actor.movementAction.direction * (speed * deltaTimeS);
        if (actor.body.velocity.lengthSquared != 0.0) {
          this._physicsEngine.activate(this.worldState, actor.body);
        }
      }
    }
  }
  
  void _updateEffects (WorldState ws) {
    for (Effect effect in ws.effects) {
      if (effect.active) {
        if (effect.body.collidable != null) {
          if (effect.body.collidable is CollidableRay) {
            CollidableRay ray = effect.body.collidable as CollidableRay;
            Vector2 target = effect.body.position + ray.target;
            DynamicBody body = this.collisionManager.raycast(effect.body.position, 
                                                              target);
            if (body != null) {
              ray.target = target - effect.body.position;
              effect.beforePenetration(effect.body, body);
            }
          } else {
            this.collisionManager.detectObjectCollisions(effect.body, 
                                                          beforeContact: effect.beforeContact, 
                                                          beforePenetration: effect.beforePenetration, 
                                                          handleCollision: effect.handleCollision);
          }
        }
        effect.update();
        if (effect.timed && effect.timer < 0) {
          // We delay removal until next update, so that effects with an initial timer = 0 have a chance to be drawn.
          effect.active = false;
        }
      } else {
        ws.removeEffect(effect);
      }
    }
  }
}
